package sip

import (
	"bytes"
	"fmt"
	"strings"
	"sync"
)

type (
	// Params Generic list of parameters on a header.
	Params interface {
		Get(key string) (MaybeString, bool)
		Add(key string, val MaybeString) Params
		Remove(key string) Params
		Clone() Params
		Equals(params any) bool
		ToString(sep uint8) string
		String() string
		Length() int
		Items() map[string]MaybeString
		Keys() []string
		Has(key string) bool
	}

	// Params implementation.
	headerParams struct {
		mu         sync.RWMutex
		params     map[string]MaybeString
		paramOrder []string
	}
)

const (
	// Whitespace recognised by SIP protocol.
	abnfWs = " \t"
)

// IMPLEMENTATION

// NewParams Create an empty set of parameters.
func NewParams() Params {
	return &headerParams{
		params:     make(map[string]MaybeString),
		paramOrder: []string{},
	}
}

// Items Returns the entire parameter map.
func (params *headerParams) Items() map[string]MaybeString {
	params.mu.RLock()
	defer params.mu.RUnlock()
	return params.params
}

// Keys Returns a slice of keys, in order.
func (params *headerParams) Keys() []string {
	params.mu.RLock()
	defer params.mu.RUnlock()
	return params.paramOrder
}

// Get Returns the requested parameter value.
func (params *headerParams) Get(key string) (MaybeString, bool) {
	params.mu.RLock()
	v, ok := params.params[key]
	params.mu.RUnlock()
	return v, ok
}

// Add Put a new parameter.
func (params *headerParams) Add(key string, val MaybeString) Params {
	params.mu.Lock()
	// Add param to order list if new.
	if _, ok := params.params[key]; !ok {
		params.paramOrder = append(params.paramOrder, key)
	}

	// Set param value.
	params.params[key] = val
	params.mu.Unlock()
	// Return the params so calls can be chained.
	return params
}

func (params *headerParams) Remove(key string) Params {
	params.mu.Lock()
	if _, ok := params.params[key]; ok {
		for k, v := range params.paramOrder {
			if v == key {
				params.paramOrder = append(params.paramOrder[:k], params.paramOrder[k+1:]...)
				break
			}
		}
		delete(params.params, key)
	}
	params.mu.Unlock()
	// Return the params so calls can be chained.
	return params
}

func (params *headerParams) Has(key string) bool {
	params.mu.RLock()
	_, ok := params.params[key]
	params.mu.RUnlock()
	return ok
}

// Clone Copy a list of params.
func (params *headerParams) Clone() Params {
	if params == nil {
		var dup *headerParams
		return dup
	}

	dup := NewParams()
	for _, key := range params.Keys() {
		if val, ok := params.Get(key); ok {
			dup.Add(key, val)
		}
	}

	return dup
}

// ToString Render params to a string.
// Note that this does not escape special characters, this should already have been done before calling this method.
func (params *headerParams) ToString(sep uint8) string {
	if params == nil {
		return ""
	}

	var buffer bytes.Buffer
	first := true

	for _, key := range params.Keys() {
		val, ok := params.Get(key)
		if !ok {
			continue
		}

		if !first {
			buffer.WriteString(fmt.Sprintf("%c", sep))
		}
		first = false

		buffer.WriteString(fmt.Sprintf("%s", Escape(key, EncodeQueryComponent)))

		if val, ok := val.(String); ok {
			valStr := val.String()
			if valStr != "" && valStr[0] == '"' && valStr[len(valStr)-1] == '"' { // already escaped header param value
				buffer.WriteString(fmt.Sprintf("=%s", valStr))
			} else if strings.ContainsAny(valStr, abnfWs) {
				buffer.WriteString(fmt.Sprintf("=\"%s\"", strings.ReplaceAll(Escape(valStr, EncodeQueryComponent), "\"", "\\\"")))
			} else {
				buffer.WriteString(fmt.Sprintf("=%s", Escape(valStr, EncodeQueryComponent)))
			}
		}
	}

	return buffer.String()
}

// String returns params joined with '&' char.
func (params *headerParams) String() string {
	if params == nil {
		return ""
	}

	return params.ToString('&')
}

// Length Returns number of params.
func (params *headerParams) Length() int {
	params.mu.RLock()
	defer params.mu.RUnlock()
	return len(params.params)
}

// Equals Check if two maps of parameters are equal in the sense of having the same keys with the same values.
// This does not rely on any ordering of the keys of the map in memory.
func (params *headerParams) Equals(other any) bool {
	q, ok := other.(*headerParams)
	if !ok {
		return false
	}

	if params == q {
		return true
	}
	if params == nil && q != nil || params != nil && q == nil {
		return false
	}

	if params.Length() == 0 && q.Length() == 0 {
		return true
	}

	if params.Length() != q.Length() {
		return false
	}

	for key, pVal := range params.Items() {
		qVal, ok := q.Get(key)
		if !ok {
			return false
		}
		if pVal != qVal {
			return false
		}
	}

	return true
}
